package org.vison.wonfu.framework.commons.utils;

import org.vison.wonfu.framework.commons.Constants;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StringUtils {

    public static final int EOF = -1;
    public static final String EMPTY_STRING = "";

    public static final String LINE_FEED_STRING = "\n";

    public static final char COMMA = ',';

    public static final String[] EMPTY_STRING_ARRAY = new String[0];

    private static final byte[] HEX2B;

    private static final String DOT_REGEX = "\\.";

    // key value pair pattern.
    private static final Pattern KVP_PATTERN = Pattern.compile("([_.a-zA-Z0-9][-_.a-zA-Z0-9]*)[=](.*)");

    private static final Pattern PATH_PATTERN = Pattern.compile("\\{(.*?)}");

    static {
        HEX2B = new byte[128];
        Arrays.fill(HEX2B, (byte) -1);
        HEX2B['0'] = (byte) 0;
        HEX2B['1'] = (byte) 1;
        HEX2B['2'] = (byte) 2;
        HEX2B['3'] = (byte) 3;
        HEX2B['4'] = (byte) 4;
        HEX2B['5'] = (byte) 5;
        HEX2B['6'] = (byte) 6;
        HEX2B['7'] = (byte) 7;
        HEX2B['8'] = (byte) 8;
        HEX2B['9'] = (byte) 9;
        HEX2B['A'] = (byte) 10;
        HEX2B['B'] = (byte) 11;
        HEX2B['C'] = (byte) 12;
        HEX2B['D'] = (byte) 13;
        HEX2B['E'] = (byte) 14;
        HEX2B['F'] = (byte) 15;
        HEX2B['a'] = (byte) 10;
        HEX2B['b'] = (byte) 11;
        HEX2B['c'] = (byte) 12;
        HEX2B['d'] = (byte) 13;
        HEX2B['e'] = (byte) 14;
        HEX2B['f'] = (byte) 15;
    }

    private StringUtils() {
    }

    public static boolean isBlank(CharSequence cs) {
        int strLen;
        if (cs == null || (strLen = cs.length()) == 0) {
            return true;
        }
        for (int i = 0; i < strLen; i++) {
            if (!Character.isWhitespace(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * is empty string.
     *
     * @param str source string.
     * @return is empty.
     */
    public static boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }

    /**
     * str="null" : true
     */
    public static boolean isNullStrOrEmpty(String str) {
        return isEmpty(str) || StringUtils.equals(str, "null");
    }

    public static boolean isEmptyValue(Object value) {
        return value == null
                || value instanceof String && StringUtils.isBlank((String) value);
    }

    /**
     * <p>Checks if the strings contain empty or null elements. <p/>
     *
     * <pre>
     * StringUtils.isNoneEmpty(null)            = false
     * StringUtils.isNoneEmpty("")              = false
     * StringUtils.isNoneEmpty(" ")             = true
     * StringUtils.isNoneEmpty("abc")           = true
     * StringUtils.isNoneEmpty("abc", "def")    = true
     * StringUtils.isNoneEmpty("abc", null)     = false
     * StringUtils.isNoneEmpty("abc", "")       = false
     * StringUtils.isNoneEmpty("abc", " ")      = true
     * </pre>
     *
     * @param ss the strings to check
     * @return {@code true} if all strings are not empty or null
     */
    public static boolean isNoneEmpty(final String... ss) {
        if (ArrayUtils.isEmpty(ss)) {
            return false;
        }
        for (final String s : ss) {
            if (isEmpty(s)) {
                return false;
            }
        }
        return true;
    }

    /**
     * <p>Checks if the strings contain at least on empty or null element. <p/>
     *
     * <pre>
     * StringUtils.isAnyEmpty(null)            = true
     * StringUtils.isAnyEmpty("")              = true
     * StringUtils.isAnyEmpty(" ")             = false
     * StringUtils.isAnyEmpty("abc")           = false
     * StringUtils.isAnyEmpty("abc", "def")    = false
     * StringUtils.isAnyEmpty("abc", null)     = true
     * StringUtils.isAnyEmpty("abc", "")       = true
     * StringUtils.isAnyEmpty("abc", " ")      = false
     * </pre>
     *
     * @param ss the strings to check
     * @return {@code true} if at least one in the strings is empty or null
     */
    public static boolean isAnyEmpty(final String... ss) {
        return !isNoneEmpty(ss);
    }


    /**
     * Is not blank string.
     */
    public static boolean isNotBlank(CharSequence cs) {
        return !isBlank(cs);
    }

    /**
     * is not empty string.
     *
     * @param str source string.
     * @return is not empty.
     */
    public static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    public static String camelToSplitName(String camelName, String split) {
        if (isEmpty(camelName)) {
            return camelName;
        }
        StringBuilder buf = null;
        for (int i = 0; i < camelName.length(); i++) {
            char ch = camelName.charAt(i);
            if (ch >= 'A' && ch <= 'Z') {
                if (buf == null) {
                    buf = new StringBuilder();
                    if (i > 0) {
                        buf.append(camelName, 0, i);
                    }
                }
                if (i > 0) {
                    buf.append(split);
                }
                buf.append(Character.toLowerCase(ch));
            } else if (buf != null) {
                buf.append(ch);
            }
        }
        return buf == null ? camelName : buf.toString();
    }

    /**
     * split.
     *
     * @param ch char.
     * @return string array.
     */
    public static String[] split(String str, char ch) {
        if (isEmpty(str)) {
            return EMPTY_STRING_ARRAY;
        }
        return splitToList0(str, ch).toArray(EMPTY_STRING_ARRAY);
    }

    private static List<String> splitToList0(String str, char ch) {
        List<String> result = new ArrayList<>();
        int ix = 0, len = str.length();
        for (int i = 0; i < len; i++) {
            if (str.charAt(i) == ch) {
                result.add(str.substring(ix, i));
                ix = i + 1;
            }
        }

        if (ix >= 0) {
            result.add(str.substring(ix));
        }
        return result;
    }

    /**
     * Splits String around matches of the given character.
     * <p>
     * Note: Compare with {@link StringUtils#split(String, char)}, this method reduce memory copy.
     */
    public static List<String> splitToList(String str, char ch) {
        if (isEmpty(str)) {
            return Collections.emptyList();
        }
        return splitToList0(str, ch);
    }

    /**
     * parse query string to Parameters.
     *
     * @param qs query string.
     * @return Parameters instance.
     */
    public static Map<String, String> parseQueryString(String qs) {
        if (isEmpty(qs)) {
            return new HashMap<>();
        }
        return parseKeyValuePair(qs, "\\&");
    }

    /**
     * parse key-value pair.
     *
     * @param str           string.
     * @param itemSeparator item separator.
     * @return key-value map;
     */
    private static Map<String, String> parseKeyValuePair(String str, String itemSeparator) {
        String[] tmp = str.split(itemSeparator);
        Map<String, String> map = new HashMap<>(tmp.length);
        for (String s : tmp) {
            Matcher matcher = KVP_PATTERN.matcher(s);
            if (!matcher.matches()) {
                continue;
            }
            map.put(matcher.group(1), matcher.group(2));
        }
        return map;
    }

    public static int decodeHexNibble(final char c) {
        // Character.digit() is not used here, as it addresses a larger
        // set of characters (both ASCII and full-width latin letters).
        byte[] hex2b = HEX2B;
        return c < hex2b.length ? hex2b[c] : -1;
    }

    /**
     * Decode a 2-digit hex byte from within a string.
     */
    public static byte decodeHexByte(CharSequence s, int pos) {
        int hi = decodeHexNibble(s.charAt(pos));
        int lo = decodeHexNibble(s.charAt(pos + 1));
        if (hi == -1 || lo == -1) {
            throw new IllegalArgumentException(String.format(
                    "invalid hex byte '%s' at index %d of '%s'", s.subSequence(pos, pos + 2), pos, s));
        }
        return (byte) ((hi << 4) + lo);
    }

    /**
     * <p>Removes a substring only if it is at the end of a source string,
     * otherwise returns the source string.</p>
     *
     * <p>A {@code null} source string will return {@code null}.
     * An empty ("") source string will return the empty string.
     * A {@code null} search string will return the source string.</p>
     *
     * <pre>
     * StringUtils.removeEnd(null, *)      = null
     * StringUtils.removeEnd("", *)        = ""
     * StringUtils.removeEnd(*, null)      = *
     * StringUtils.removeEnd("www.domain.com", ".com.")  = "www.domain.com"
     * StringUtils.removeEnd("www.domain.com", ".com")   = "www.domain"
     * StringUtils.removeEnd("www.domain.com", "domain") = "www.domain.com"
     * StringUtils.removeEnd("abc", "")    = "abc"
     * </pre>
     *
     * @param str    the source String to search, may be null
     * @param remove the String to search for and remove, may be null
     * @return the substring with the string removed if found, {@code null} if null String input
     */
    public static String removeEnd(final String str, final String remove) {
        if (isEmpty(str) || isEmpty(remove)) {
            return str;
        }
        if (str.endsWith(remove)) {
            return str.substring(0, str.length() - remove.length());
        }
        return str;
    }

    /**
     * <p>Removes a substring only if it is at the beginning of a source string,
     * otherwise returns the source string.</p>
     *
     * <p>A {@code null} source string will return {@code null}.
     * An empty ("") source string will return the empty string.
     * A {@code null} search string will return the source string.</p>
     *
     * <pre>
     * StringUtils.removeStart(null, *)      = null
     * StringUtils.removeStart("", *)        = ""
     * StringUtils.removeStart(*, null)      = *
     * StringUtils.removeStart("www.domain.com", "www.")   = "domain.com"
     * StringUtils.removeStart("domain.com", "www.")       = "domain.com"
     * StringUtils.removeStart("www.domain.com", "domain") = "www.domain.com"
     * StringUtils.removeStart("abc", "")    = "abc"
     * </pre>
     *
     * @param str    the source String to search, may be null
     * @param remove the String to search for and remove, may be null
     * @return the substring with the string removed if found, {@code null} if null String input
     */
    public static String removeStart(final String str, final String remove) {
        if (isEmpty(str) || isEmpty(remove)) {
            return str;
        }
        if (str.startsWith(remove)) {
            return str.substring(remove.length());
        }
        return str;
    }

    /**
     * Throwable toString
     */
    public static String toString(Throwable e) {
        StringWriter w = new StringWriter();
        PrintWriter p = new PrintWriter(w);
        p.print(e.getClass().getName());
        if (e.getMessage() != null) {
            p.print(": " + e.getMessage());
        }
        p.println();
        try {
            return w.toString();
        } finally {
            p.close();
        }
    }


    /**
     * If the given string does not begin with prefix, prefix is added at the beginning
     */
    public static String addPrefixIfNot(String str, String prefix) {
        if (isEmpty(str) || isEmpty(prefix)) {
            return str;
        }

        if (!str.startsWith(prefix)) {
            return prefix.concat(str);
        }
        return str;
    }

    /**
     * If the given string does not end in suffix, add suffix at the end
     */
    public static String addSuffixIfNot(String str, String suffix) {
        if (isEmpty(str) || isEmpty(suffix)) {
            return str;
        }

        if (!str.endsWith(suffix)) {
            return str.concat(suffix);
        }
        return str;
    }


    /**
     * 格式化路径，将路径中的占位符替换为实际值
     * @param template 路径模板，例如："/{application}/{service}/{method}"
     * @param params 路径参数，例如：{application: "admin", service: "user", method: "get"}
     * @return 格式化后的路径，例如："/admin/user/get"
     */
    public static String pathFormat(String template, Map<String, Object> params) {
        if (StringUtils.isEmpty(template) || CollectionUtils.isEmptyMap(params)) {
            return template;
        }
        Matcher matcher = PATH_PATTERN.matcher(template);
        while (matcher.find()) {
            String group = matcher.group(1);
            Object objectValue = params.get(group);
            String value = objectValue == null ? null : (String) Optional.of(objectValue)
                    .map(v -> v.getClass().isAssignableFrom(String.class) ? v : String.valueOf(v)).get();
            if (isNotEmpty(value)) {
                template = template.replace(matcher.group(), value);
                params.remove(group);
            }
        }
        return template;
    }

     /**
     * 从路径模板中提取占位符变量名
     * @param pathTemplate 路径模板，例如："/{application}/{service}/{method}"
     * @return 占位符变量名集合，例如：["application", "service", "method"]
     */
    public static Set<String> extractPathVariables(String pathTemplate) {
        if (isEmpty(pathTemplate)) {
            return Collections.emptySet();
        }
        Matcher matcher = PATH_PATTERN.matcher(pathTemplate);
        Set<String> variables = new LinkedHashSet<>();
        while (matcher.find()) {
            variables.add(matcher.group(1));
        }
        return variables;
    }



    public static boolean equals(CharSequence str1, CharSequence str2) {
        return equals(str1, str2, false);
    }


    /**
     * 比较两个字符串是否相等。
     *
     * @param str1       要比较的字符串1
     * @param str2       要比较的字符串2
     * @param ignoreCase 是否忽略大小写
     * @return 如果两个字符串相同，或者都是{@code null}，则返回{@code true}
     * @since 3.2.0
     */
    public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) {
        if (null == str1) {
            // 只有两个都为null才判断相等
            return str2 == null;
        }
        if (null == str2) {
            // 字符串2空，字符串1非空，直接false
            return false;
        }

        if (ignoreCase) {
            return str1.toString().equalsIgnoreCase(str2.toString());
        } else {
            return str1.toString().contentEquals(str2);
        }
    }


    public static String toLowerCaseFirstOne(String s) {
        if (isEmpty(s) || Character.isLowerCase(s.charAt(0))) {
            return s;
        }
        if (s.length() == 1) {
            return s.toLowerCase();
        }
        return Character.toLowerCase(s.charAt(0)) + s.substring(1);
    }


    public static boolean isEmptyApplication(String application) {
        return StringUtils.isEmpty(application) || Constants.UNKNOWN_APPLICATION_NAME.equals(application);
    }

    public static String substringAfterLast(String str, String separator) {
        if (isEmpty(str)) {
            return str;
        }
        if (isEmpty(separator)) {
            return EMPTY_STRING;
        }
        int pos = str.lastIndexOf(separator);
        if (pos == -1 || pos == (str.length() - separator.length())) {
            return EMPTY_STRING;
        }
        return str.substring(pos + separator.length());
    }

    public static String substringBeforeLast(String str, String separator) {
        if (isEmpty(str) || isEmpty(separator)) {
            return str;
        }
        int pos = str.lastIndexOf(separator);
        if (pos == -1) {
            return str;
        }
        return str.substring(0, pos);
    }

    public static String readFromInputStream(InputStream inputStream) {
        return readFromInputStream(inputStream, Charset.defaultCharset());
    }

    public static String readFromInputStream(InputStream inputStream, Charset charset) {
        if (inputStream == null) {
            return "";
        }

        try (StringWriter writer = new StringWriter();
             InputStreamReader reader = new InputStreamReader(inputStream, charset);
             BufferedReader bufferedReader = new BufferedReader(reader)) {
            char[] chars = new char[1024];
            int readChars;
            while ((readChars = bufferedReader.read(chars)) != EOF) {
                writer.write(chars, 0, readChars);
            }
            return writer.toString();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * return empty string when str is null
     *
     * @param str source string.
     */
    public static String emptyIfNull(String str) {
        return str == null ? EMPTY_STRING : str;
    }
}
